/*----------------------------------------------------------------------------
 | Copyright (c) 2009 Matthew B. Brophy (matt@brophy.org).  
 | Liscense: MIT License (http://www.opensource.org/licenses/mit-license.php)
 |
 | $Id: util.js 37 2010-04-05 18:25:24Z matt@brophy.org $
 -----------------------------------------------------------------------------*/

if (typeof org == "undefined" || 
    org == null) { org = {}; }
if (typeof org.brophy == "undefined" || 
    org.brophy == null) { org.brophy = {}; }
if (typeof org.brophy.js == "undefined" || 
    org.brophy.js == null) { org.brophy.js = {}; }

/** 
 * @namespace 
 * @description Utilities namespace in the org.brophy.js JavaScript library 
 * @requires    This library requires the <a href="http://www.mochikit.com">
 *              MochiKit</a> JavaScript library 
 *              (version 1.4+).
 */
org.brophy.js.util = {};

/**
 * Toggle the display of an HTML element on the page.  This is done by 
 * toggling the presence of a css class, which contains a 'display:none;' 
 * rule
 *
 * @param  elemOrId  
 *         Either the element to hide/show, or the ID of an element on the 
 *         page.  If this parameter is a string, the element will be 
 *         looked up with getElementById().
 * @param  [fancy = false]
 *         Optional boolean parameter on whether use MochiKit.Visual 
 *         effects to hide the element in a more vidsually epealling way
 * @param  [duration = 0.5]
 *         Optional duraiton of the effect if the 'fancy; parameter is 
 *         TRUE.  Defaults to 0.5 seconds.
 * @return TRUE if the Element is found and the display is toggled, 
 *         FALSE otherwise.
 * @type   boolean
 */
org.brophy.js.util.toggleDisplay = function(elemOrId, fancy, duration)
{ 
  if(typeof elemOrId == "string") 
    { elemOrId = MochiKit.DOM.getElement(elemOrId); }

  if(MochiKit.Base.isUndefinedOrNull(fancy))    { fancy = false; }
  if(MochiKit.Base.isUndefinedOrNull(duration)) { duration = 0.5; }

  if( !MochiKit.Base.isUndefinedOrNull(elemOrId) &&
      (elemOrId.nodeType == org.brophy.js.constant.NODE_TYPE_ELEMENT) ) 
  { 
    if(fancy)
    {
      if( MochiKit.DOM.hasElementClass(elemOrId, "obj_none") || 
          elemOrId.style.display == "none" ) 
      { 
        MochiKit.Visual.blindDown(
          elemOrId,
          { "afterFinish" : 
               function() 
                { MochiKit.DOM.removeElementClass(elemOrId, "obj_none"); },
            "duration" : duration
          });
      }
      else 
      { 
          MochiKit.Visual.blindUp(elemOrId, { "duration" : duration });
      }
    }
    else
    {
      MochiKit.DOM.toggleElementClass("obj_none", elemOrId); 
    }
    return true;
  }
  return false;
}

/**
 * @private
 * Utility function to write debug messages to the bottom of the page.  Good
 * substitute for alert() in IE.
 *
 * @return None
 */
org.brophy.js.util.debug = function(msg)
{
  if( MochiKit.Base.isUndefinedOrNull(
    MochiKit.DOM.getElement("obj_logging_div")) )
  {
    MochiKit.DOM.appendChildNodes(document.body,
                   MochiKit.DOM.DIV( { "id" : "obj_logging_div" }, null));
  }

  MochiKit.Logging.log(msg);

  MochiKit.DOM.appendChildNodes(
    MochiKit.DOM.getElement("obj_logging_div"), 
    MochiKit.DOM.P(msg));

}

/**
 * Get the left edge of an element (in pixels) relative to the left edge 
 * of the document.
 * @param   e
 *          The element in question
 * @return  The number of pixels from the left edge of the document 
 *          to the left edge of the element.
 * @type    integer
 */
org.brophy.js.util.getLeft = function(elem)
{
  return Math.round(MochiKit.Style.getElementPosition(elem).x);
  /*
  var left = 0;
  while(elem !== null && elem.offsetLeft > 0) 
  { 
    org.brophy.js.util.debug("Adding offsetleft");
    left += elem.offsetLeft;
    elem = elem.offsetParent;
  }
  return left;
  */
}

/**
 * Get the top edge of an element (in pixels) relative to the top edge 
 * of the document.
 * @param   e
 *          The element in question
 * @return  The number of pixels from the top edge of the document 
 *          to the top edge of the element.
 * @type    integer
 */
org.brophy.js.util.getTop = function(elem)
{
  return Math.round(MochiKit.Style.getElementPosition(elem).y);
  /*
  var top = 0;
  while(elem !== null && elem.offsetTop > 0) 
  { 
    top += elem.offsetTop;
    elem = elem.offsetParent;
  }
  return top;
  */
}

/**
 * Get the left edge of an element (in pixels) relative to the left edge 
 * of the current viewport.
 * @param   e
 *          The element in question
 * @return  The number of pixels from the left edge of the viewport 
 *          to the left edge of the element.
 * @type    integer
 */
org.brophy.js.util.getRelativeLeft = function(elem)
{
  return Math.round(
           MochiKit.Style.getElementPosition(elem).x - 
           org.brophy.js.util.getScrollX() );
  /*
  var left = 0;
  while(elem !== null && elem.offsetLeft > 0) 
  { 
    org.brophy.js.util.debug("Adding offsetleft");
    left += elem.offsetLeft;
    elem = elem.offsetParent;
  }
  return left;
  */
}

/**
 * Get the top edge of an element (in pixels) relative to the top edge 
 * of the current viewport.
 * @param   e
 *          The element in question
 * @return  The number of pixels from the top edge of the viewport 
 *          to the top edge of the element.
 * @type    integer
 */
org.brophy.js.util.getRelativeTop = function(elem)
{
  return Math.round(
           MochiKit.Style.getElementPosition(elem).y - 
           org.brophy.js.util.getScrollY() );
  /*
  var top = 0;
  while(elem !== null && elem.offsetTop > 0) 
  { 
    top += elem.offsetTop;
    elem = elem.offsetParent;
  }
  return top;
  */
}

/**
 * Get the number of pixels scrolled horizontally
 *
 * @return  The number of pixels scrolled right
 * @type    integer
 */
org.brophy.js.util.getScrollX = function()
{
  var o = MochiKit.Style.getViewportPosition();
  return ( MochiKit.Base.isUndefinedOrNull(o) || 
           MochiKit.Base.isUndefinedOrNull(o.x) ) ?
         0 : o.x;
}

/**
 * Get the number of pixels scrolled horizontally
 *
 * @return  The number of pixels scrolled right
 * @type    integer
 */
org.brophy.js.util.getScrollY = function()
{
  var o = MochiKit.Style.getViewportPosition();
  return ( MochiKit.Base.isUndefinedOrNull(o) || 
           MochiKit.Base.isUndefinedOrNull(o.y) ) ?
         0 : o.y;
}

/**
 * Get the width of the current window
 *
 * @return  The width of the current window in pixels
 * @type    integer
 */
org.brophy.js.util.getWindowWidth = function()
{
  var o = MochiKit.Style.getViewportDimensions();
  return ( MochiKit.Base.isUndefinedOrNull(o) || 
           MochiKit.Base.isUndefinedOrNull(o.w) ) ?
         0 : o.w;
}

/**
 * Get the width of the current window
 *
 * @return  The width of the current window in pixels
 * @type    integer
 */
org.brophy.js.util.getWindowHeight = function()
{
  var o = MochiKit.Style.getViewportDimensions();
  return ( MochiKit.Base.isUndefinedOrNull(o) || 
           MochiKit.Base.isUndefinedOrNull(o.h) ) ?
         0 : o.h;
}

/**
 * Remove leading and trailing spaces from a string.
 *
 * @param str  The string to trim
 * @return The trimmed string
 * @type String
 */
org.brophy.js.util.trim = function(str)
{
  return MochiKit.Base.isUndefinedOrNull(str) ? 
         "" : str.replace(/^\s+|\s+$/g,"");
}

/**
 * Remove leading spaces from a string.
 *
 * @param str  The string to trim
 * @return The trimmed string
 * @type String
 */
org.brophy.js.util.triml = function(str)
{
  return MochiKit.Base.isUndefinedOrNull(str) ? 
         "" : str.replace(/^\s+/g,"");
}

/**
 * Remove trailing spaces from a string.
 *
 * @param str  The string to trim
 * @return The trimmed string
 * @type String
 */
org.brophy.js.util.trimr = function(str)
{
  return MochiKit.Base.isUndefinedOrNull(str) ? 
         "" : str.replace(/\s+$/g,"");
}

/**
 * Codify all elements of the given class name.  This is meant 
 * to be called on page load on a DIV or P element with this 
 * class name.  IT will set the background colors of different 
 * lines to alternating colors and add lines numbers.
 * 
 * @param The classname of the elements to be codified
 * @return none
 */
org.brophy.js.util.codify = function(clsName)
{
  var nodes = MochiKit.Selector.findDocElements("." + clsName);
  MochiKit.Iter.forEach(nodes, function(n) 
  {
    if(n.nodeType != org.brophy.js.constant.NODE_TYPE_ELEMENT ||
       n.childNodes.length > 1)
        { return; }

    // Get the value of the child text node
    var codeStr = n.firstChild.nodeValue;

    // Force white space
    codeStr = org.brophy.js.util.trimr(codeStr);
    if(codeStr.substring(0,1) == "\n")
        { codeStr = codeStr.substring(1); }

    codeStr = codeStr.replace(/ /g, "\u00a0");
    
    // Remove the children, add CSS classes, add first blank line
    MochiKit.DOM.replaceChildNodes(n);
    MochiKit.DOM.addElementClass(n, "obj_code_container");
    MochiKit.DOM.addElementClass(n, "obj_code");
    MochiKit.DOM.appendChildNodes(n,
                 MochiKit.DOM.DIV( { "class" : "obj_code_line_odd" }, 
                 MochiKit.DOM.BR()));

    // Loop through lines alternating background colors and adding
    // line numbers.
    var numSpaces = 0;
    var indentStr = "";
    var lines = codeStr.split("\n");  
    for(var i = 0; i < lines.length; i++)
    {
      // If this is the first line, let's set up our indention, 
      // i.e. how many spaces to ignore at the beginning of a string
      if(i == 0)
      {
        var tempStr = org.brophy.js.util.triml(lines[i]);
        numSpaces = lines[i].length - tempStr.length;
      }

      if(org.brophy.js.util.triml(lines[i].substring(0, numSpaces)).length == 0)
      {
        lines[i] = lines[i].substring(numSpaces);
      }
      
      var isEven = (i%2) == 0;
      var className = isEven ? "obj_code_line_even" : "obj_code_line_odd";

      var spc = org.brophy.js.constant.UNICODE_NBSP;
      var lineDiv = MochiKit.DOM.DIV( { "class" : className }, 
                      spc + (i+1) + ":" + spc + lines[i] + spc + spc);
      MochiKit.DOM.appendChildNodes(n, lineDiv);
    }

    // Add trailing blank line
    var className = (lines.length%2) == 0 ? 
                    "obj_code_line_even" : 
                    "obj_code_line_odd";
    MochiKit.DOM.appendChildNodes(n,
                 MochiKit.DOM.DIV( { "class" : className }, MochiKit.DOM.BR()));    
  });
}


/**
 * Advanced typeOf function to use instead of the built in typeof operator.
 * Compliments of Douglas Crockford:
 *   http://javascript.crockford.com/remedial.html
 *
 * @param value The variable to determine the type of
 * @return A string in the set (object,function,string,number,undefined,array,null)
 * @return string
 */
org.brophy.js.util.typeOf = function(value)
{
    var s = typeof value;
    if (s === 'object') {
        if (value) {
            if (typeof value.length === 'number' &&
                    !(value.propertyIsEnumerable('length')) &&
                    typeof value.splice === 'function') {
                s = 'array';
            }
        } else {
            s = 'null';
        }
    }
    return s;
}

org.brophy.js.util.previousElement = function(elem)
{
  while((elem = elem.previousSibling) != null)
  {
    if(elem.nodeType === 1) { return elem; }
  }
  return null;
}

org.brophy.js.util.previousElements = function(elem)
{
  var arr = [];
  while((elem = elem.previousSibling) != null)
  {
    if(elem.nodeType === 1) { arr.push(elem); }
  }
  return arr;
}

org.brophy.js.util.nextElement = function(elem)
{
  while((elem = elem.nextSibling) != null)
  {
    if(elem.nodeType === 1) { return elem; }
  }
  return null;
}

org.brophy.js.util.nextElements = function(elem)
{
  var arr = [];
  while((elem = elem.nextSibling) != null)
  {
    if(elem.nodeType === 1) { arr.push(elem); }
  }
  return arr;
}

org.brophy.js.util.getFirstElementChildNode = function(node)
{
  var n = MochiKit.Base.isUndefinedOrNull;
  if(n(node) || n(node.childNodes))
    { throw "Invalid node parameter to getFirstElementChildNode()."; }

  for(var i = 0; i < node.childNodes.length; i++)
  {
    var child = node.childNodes[i];
    if(child.nodeType === org.brophy.js.constant.NODE_TYPE_ELEMENT)
      { return child; }
  }
  return null;
}

org.brophy.js.util.insertFirstChildNode = function(parent, child)
{
  var firstChild = org.brophy.js.util.getFirstElementChildNode(parent);
  if(firstChild === null)
    { appendChildNodes(parent, child); }
  else
    { insertSiblingNodesBefore(firstChild, child); }
  return true;
}

org.brophy.js.util.forEachElementChildNode = function(node,func)
{
  MochiKit.Iter.forEach(node.childNodes,
                        function(c){if(c.nodeType == 1){func(c);}});
}

org.brophy.js.util.deselectAll = function()
{
  // More efficient way, but produces errors in FF?
  //if (window.getSelection() && window.getSelection().getRangeAt(0))
  //  { window.getSelection().removeAllRanges(); }
  //else if (document.selection && document.selection.createRange().text)
  //  { document.selection.empty(); }
  if (window.getSelection)
    { window.getSelection().removeAllRanges(); }
  else if (document.selection)
    { document.selection.empty(); }
}

/**
 * Trim any trailing non-digit characters off of 'str'
 *   15px  -> 15
 *   15em  -> 15
 *   15%   -> 15
 * @return The leading integer portion of the string
 */
org.brophy.js.util.trimCssUnits = function(str)
{
  str = str.replace(/([0-9]*)(.*)/, "$1");
  return str;
}

/**
 * Return a random integer x such that low &lt;= x &lt;= high
 */
org.brophy.js.util.random = function(low,high)
{
  return Math.floor(Math.random()*((high-low)+1)) + low;
}


/**
 * Find the nearest ancestor of 'node' for which isNodeFunc(ancestor) returns
 * true.  isNodeFunc is meant to be a single-parameter function which checks
 * the given ancestor node and returns true if this is the ancestor node you
 * are looking for.
 */
org.brophy.js.util.findAncestor = function(node, isNodeFunc)
{
  if(isUndefinedOrNull(node)) { throw "node is undefined or null"; }
  if(isUndefinedOrNull(isNodeFunc)) { throw "isNodeFunc is undefined or null"; }

  var parent = node.parentNode;
  while(parent !== null && !isNodeFunc(parent))
    { parent = parent.parentNode; }
  return parent;
}

/**
 * Find the closest sibling after 'node' for which isNodeFunc(sibling) returns
 * true.  isNodeFunc is meant to be a single-parameter function which checks
 * the given ancestor node and returns true if this is the sibling node you
 * are looking for.
 */
org.brophy.js.util.findSiblingAfter = function(node, isNodeFunc)
{
  if(isUndefinedOrNull(node)) { throw "node is undefined or null"; }
  if(isUndefinedOrNull(isNodeFunc)) { throw "isNodeFunc is undefined or null"; }

  node = node.nextSibling;
  while(node !== null && !isNodeFunc(node))
    { node = node.nextSibling; }
  return node;
}

/**
 * Find the closest sibling before 'node' for which isNodeFunc(sibling) returns
 * true.  isNodeFunc is meant to be a single-parameter function which checks
 * the given ancestor node and returns true if this is the sibling node you
 * are looking for.
 */
org.brophy.js.util.findSiblingBefore = function(node, isNodeFunc)
{
  if(isUndefinedOrNull(node)) { throw "node is undefined or null"; }
  if(isUndefinedOrNull(isNodeFunc)) { throw "isNodeFunc is undefined or null"; }

  node = node.previousSibling;
  while(node !== null && !isNodeFunc(node))
    { node = node.previousSibling; }
  return node;
}

/**
 * Find the nearest ancestor of 'node' with the given 'tagName', or return
 * null if no such ancestor exists
 */
org.brophy.js.util.findAncestorByTagName = function(node, tagName)
{
  if(isUndefinedOrNull(node)) { throw "node is undefined or null"; }
  if(isUndefinedOrNull(tagName)) { throw "tagName is undefined or null"; }

  return org.brophy.js.util.findAncestor(node,
    function(n) { return MochiKit.Base.compare(n.tagName, tagName) === 0; } )
}

/**
 * Get the 0-indexed column that this TD occurs in it's enclosing table.
 * This can then be used to access other TD's in this column by using:
 *   previousRow.getElementsByTagName("TD")[getTdIndex(currentTD)]
 */
org.brophy.js.util.getTdIndex = function(td)
{
  if(isUndefinedOrNull(td)) { throw "'td' is undefined or null"; }
  else if(td.tagName !== "TD") { throw "'td' must be a <TD> element"; }

  var tdIndex = 0;
  var elem = td;
  while(elem.previousSibling != null)
  {
    elem = elem.previousSibling;
    if(elem.nodeType == org.brophy.js.constant.NODE_TYPE_ELEMENT &&
       elem.tagName === "TD")
      { tdIndex++; }
  }
  return tdIndex;
}

/**
 * Apply the CSS styles in styleObj to the row and column for the given td
 * element.  styleObj is expected to be a MochiKit-like style object:
 *   { 'color' : 'red', 'background-color' : 'blue' }
 */
org.brophy.js.util.setRowAndColumnStyle = function(td, styleObj)
{
  if(isUndefinedOrNull(td)) { throw "'td' is undefined or null" }
  var tr = org.brophy.js.util.findAncestorByTagName(td, "TR");
  setStyle(tr, styleObj);

  var tdIndex = org.brophy.js.util.getTdIndex(td);
  var table = org.brophy.js.util.findAncestorByTagName(tr, "TABLE");
  var trs = table.getElementsByTagName("TR");
  forEach(trs, function(tr)
  {
    var ths = tr.getElementsByTagName("TH");
    if(ths.length > tdIndex) { setStyle(ths[tdIndex], styleObj)}
    var tds = tr.getElementsByTagName("TD");
    if(tds.length > tdIndex) { setStyle(tds[tdIndex], styleObj)}
  });
  return true;
}

org.brophy.js.util.encodeXMLEntities = function(str)
{
  str = str.replace(/&/g, "&amp;");
  str = str.replace(/"/g, "&quot;");
  str = str.replace(/'/g, "&apos;");
  str = str.replace(/</g, "&lt;");
  str = str.replace(/>/g, "&gt;");
  return str;
}

org.brophy.js.util.decodeXMLEntities = function(str)
{
  str = str.replace(/&quot;/g, "\"");
  str = str.replace(/&apos;/g, "'");
  str = str.replace(/&lt;/g, "<");
  str = str.replace(/&gt;/g, ">");
  str = str.replace(/&amp;/g, "&");
  return str;
}

org.brophy.js.util.selectCheckbox = function(checkbox)
{
  checkbox.checked = true;
  MochiKit.DOM.setNodeAttribute(checkbox, "checked", "checked");
}

org.brophy.js.util.unselectCheckbox = function(checkbox)
{
  checkbox.checked = false;
  MochiKit.DOM.setNodeAttribute(checkbox, "checked", null);
}

/**
 * Select or deselect all checkboxes beneath the optional 'parent' 
 * node.  
 * 
 *  - 'parent': The parent node from whiehc to begin the depth search 
 *              for checkboxes.  If not specified, It will default to 
 *              document.body
 *  - 'select': Boolean value to force a selection or deselection.  If not 
 *              specified, then all currently selected checkboxes will be 
 *              unselected.  If no checkboxes are currently selected, then 
 *              they will all be selected.
 */
org.brophy.js.util.selectAllCheckboxes = function(parent, select)
{
  var MB = MochiKit.Base;
  var MI = MochiKit.Iter;
  var MS = MochiKit.Selector;
  var n = MB.isUndefinedOrNull;

  if(n(parent)) { parent = document.body; }

  var checkboxes = MS.findChildElements(parent, ["input[type=checkbox]"]);
  var foundChecked = false;
  MI.forEach(checkboxes, function(c) {
    if(!n(select))
    {
      if(select === true)
        { org.brophy.js.util.selectCheckbox(c); }
      else if(select === false)
        { org.brophy.js.util.unselectCheckbox(c); }
    }
    else if(c.checked)
      { foundChecked = true; }
  });

  if(!n(select)) { return; }

  MI.forEach(checkboxes, function(c) {
    if(!foundChecked)
      { org.brophy.js.util.selectCheckbox(c); }
    else
      { org.brophy.js.util.unselectCheckbox(c); }
  });
}

org.brophy.js.util.STICKY_TABLE_HEADERS = {};

org.brophy.js.util.getOnScrollFunc = function(table, styleFunc)
{
  return function(e) {
    var n = MochiKit.Base.isUndefinedOrNull;
    var tbl = org.brophy.js.util.getStickyTableHeader(table);
    if(tbl == null) { return; }
    if(getElementPosition(table).y < getViewportPosition().y)
    {
      if(!n(tbl.parentNode)) { return; }
      /* TODO: Check in IE7? */
      setStyle(tbl, {'position':'fixed'});
      if(!n(styleFunc) && org.brophy.js.util.typeOf(styleFunc) === "function")
        { styleFunc(tbl); }
      appendChildNodes(document.body, tbl);
      setElementPosition(tbl, {'y' : '0'});
    }
    else
    {
      if(!isUndefinedOrNull(tbl.parentNode)) { tbl = removeElement(tbl); }
    }
  };
}

/**
 * Create a sticky table header for the given 'table' element.  styleFunc'
 * can be specified as a function to call with the contructed TABLE object
 * storing the sticky header row, in case you need to perform additional
 * styling on the row.
 */
org.brophy.js.util.createStickyTableHeader = function(table, styleFunc)
{
  connect(window, "onscroll", org.brophy.js.util.getOnScrollFunc(table, styleFunc));
}

/**
 * Get the cloned header row that will be used once the actual header row has
 * scrolled off of the page.  This will cache a local copy once created for
 * the first time
 */
org.brophy.js.util.getStickyTableHeader = function(table)
{
  var n = isUndefinedOrNull;
  var cloneTableId = "__clonedTable" + getNodeAttribute(table, "id");

  // Return cached version if we have it...
  if(!n(org.brophy.js.util.STICKY_TABLE_HEADERS[cloneTableId]))
      { return org.brophy.js.util.STICKY_TABLE_HEADERS[cloneTableId]; }

  if(n(table)) { return null; }

  // Find the right header row to use
  var tr;
  try {
    // Use the first row from THEAD if there is one, since THEAD can appear 
    // below the TBODY
    var theadRows = findChildElements(table, ["thead tr"]);
    if(!n(theadRows) && theadRows.length !== 0) {
      tr = theadRows[0];
    }
    else {
      // Otherwise, use the first TR node found
      tr = findChildElements(table, ["tr"])[0];
    }
  }
  catch(e) { return null; }

  if(n(tr)) { return null; }

  // Clone the hierarcy from the TABLE to the chosen header row
  var cloneTable, cloneTr;
  cloneTable = org.brophy.js.util.cloneHierarchy(table, tr, true);
  if(n(cloneTable)) { return null; }
  
  try { cloneTr = findChildElements(cloneTable, ["tr"])[0]; }
  catch(ex) { return null; }

  // Go through and set static widths for the TD nodes, since they wont have
  // additional rows set their width off of
  var tdNodes = findChildElements(tr, ["th", "td"]);
  var cloneTdNodes = findChildElements(cloneTr, ["th", "td"]);
  for(var i = 0; i < tdNodes.length; i++) {
    setElementDimensions(cloneTdNodes[i],
                         {'w':getElementDimensions(tdNodes[i]).w+1});
  }

  setNodeAttribute(cloneTable, "id", cloneTableId);
  setNodeAttribute(cloneTable, "class", getNodeAttribute(table, "class"));
  setElementDimensions(cloneTable, {'w':getElementDimensions(table).w-1});
  setElementPosition(cloneTable, {'x':getElementPosition(table).x});

  // Store a cached version for reuse
  org.brophy.js.util.STICKY_TABLE_HEADERS[cloneTableId] = cloneTable;
  return cloneTable;
}

/**
 * Clone the hierarchy from 'parent' node down to 'child' node.  If deepChild
 * is specified and TRUE, then perform a deep clone of the child.  All clones
 * between parent and child will be shallow.
 *
 * Return: The cloned 'parent' node, with a child tree leading down to 'child',
 * and beyond if 'deepChild' was specified as TRUE
 */
org.brophy.js.util.cloneHierarchy = function(parent, child, deepChild)
{
  var n = isUndefinedOrNull;
  if(n(deepChild) || deepChild !== true) { deepChild = false; }
  if(n(child) || n(child.parentNode)) { return null; }
  var cloned, prevCloned; // Loop variables
  var localParent = child.parentNode; // Local parent as we traverse up
  while(localParent !== parent.parentNode) {
    // Perform a deep clone if this is our first time through and deepChild 
    // was specified as TRUE
    cloned = localParent.cloneNode(n(cloned) && deepChild);

    // Add it to the previous clone if we have one
    if(!n(prevCloned)) { replaceChildNodes(cloned, prevCloned); }

    // Set for the next iteration of the loop
    prevCloned = cloned;
    localParent = localParent.parentNode;
  }

  return cloned;
}